// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: May 5 2010 

#region

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Xml;
using mshtml;

#endregion

namespace Salient.Web
{
    ///<summary>
    ///</summary>
    public class HtmlForm
    {

        #region Fields

        private readonly List<IHTMLElement> _all;

        private readonly List<HTMLButtonElementClass> _buttons;

        private XmlDocument _controlState;

        private readonly HTMLFormElement _form;

        private readonly List<HTMLInputElementClass> _inputs;

        private readonly List<HTMLSelectElementClass> _selects;

        private readonly List<HTMLTextAreaElementClass> _textAreas;

        private XmlDocument _viewState;

        #endregion

        #region Constructors

        ///<summary>
        ///</summary>
        ///<param name="formNode"></param>
        public HtmlForm(HTMLFormElement formNode)
        {
            _form = formNode;

            _inputs =
                new List<HTMLInputElementClass>(formNode.getElementsByTagName("input").Cast<HTMLInputElementClass>());
            _textAreas =
                new List<HTMLTextAreaElementClass>(
                    formNode.getElementsByTagName("textarea").Cast<HTMLTextAreaElementClass>());
            _selects =
                new List<HTMLSelectElementClass>(formNode.getElementsByTagName("select").Cast<HTMLSelectElementClass>());
            _buttons =
                new List<HTMLButtonElementClass>(formNode.getElementsByTagName("button").Cast<HTMLButtonElementClass>());
            _all = new List<IHTMLElement>();
            _all.AddRange(_inputs.Cast<IHTMLElement>());
            _all.AddRange(_textAreas.Cast<IHTMLElement>());
            _all.AddRange(_selects.Cast<IHTMLElement>());
            _all.AddRange(_buttons.Cast<IHTMLElement>());

            ParseViewState();
        }

        #endregion

        #region Properties

        ///<summary>
        ///</summary>
        public string Action
        {
            get { return Form.action; }
            set { Form.action = value; }
        }

        ///<summary>
        ///</summary>
        public List<IHTMLElement> All
        {
            get { return _all; }
        }

        ///<summary>
        ///</summary>
        public List<HTMLButtonElementClass> Buttons
        {
            get { return _buttons; }
        }

        ///<summary>
        ///</summary>
        public XmlDocument ControlState
        {
            get { return _controlState; }
        }

        ///<summary>
        ///</summary>
        public HTMLFormElement Form
        {
            get { return _form; }
        }

        ///<summary>
        ///</summary>
        public List<HTMLInputElementClass> Inputs
        {
            get { return _inputs; }
        }

        ///<summary>
        ///</summary>
        public NameValueCollection PostData
        {
            get { return Fields(false, false); }
        }

        ///<summary>
        ///</summary>
        public List<HTMLSelectElementClass> Selects
        {
            get { return _selects; }
        }

        ///<summary>
        ///</summary>
        public List<HTMLTextAreaElementClass> TextAreas
        {
            get { return _textAreas; }
        }

        ///<summary>
        ///</summary>
        public XmlDocument ViewState
        {
            get { return _viewState; }
        }

        #endregion

        #region Public Methods

        ///<summary>
        ///</summary>
        ///<param name="includeDisabled"></param>
        ///<param name="includeUnselected"></param>
        ///<returns></returns>
        ///<exception cref="NotImplementedException"></exception>
        public NameValueCollection Fields(bool includeDisabled, bool includeUnselected)
        {
            NameValueCollection values = new NameValueCollection();

            foreach (IHTMLElement node in _all)
            {
                if ((bool)node.getAttribute("disabled", 0) && !includeDisabled)
                {
                    continue;
                }

                // priority of form field identifier attributes is name->id
                string nodeName = (string)node.getAttribute("name", 0);
                if (string.IsNullOrEmpty(nodeName))
                {
                    nodeName = node.id;
                }

                switch (node.tagName.ToLower())
                {
                    case "input":
                        string inputType = ((string)node.getAttribute("type", 0)).ToLower();
                        if ((inputType == "radio" || inputType == "checkbox") && !includeUnselected &&
                            !(bool)node.getAttribute("checked", 0))
                        {
                            break;
                        }
                        values.Add(nodeName, (string)node.getAttribute("value", 0));
                        break;

                    case "button":
                    case "textarea":
                    case "image":
                        values.Add(nodeName, (string)node.getAttribute("value", 0));
                        break;

                    case "select":
                        IHTMLElementCollection options = ((IHTMLElement2)node).getElementsByTagName("option");
                        if (options != null)
                        {
                            foreach (IHTMLElement option in options)
                            {
                                if (((bool)option.getAttribute("disabled", 0) && !includeDisabled) ||
                                    (!(bool)option.getAttribute("selected", 0) && !includeUnselected))
                                {
                                    continue;
                                }

                                string optionValue = (string)option.getAttribute("value", 2);
                                // not sure how to handle this now. typically I would swap in
                                // the option text, but what if the value is null. I guess I will
                                // have to build some tests
                                // TODO: write some tests to determine how best to deal with a select
                                values.Add(nodeName, optionValue);
                            }
                        }

                        break;
                    default:
                        throw new NotImplementedException(string.Format("Input type {0} not supported",
                                                                        node.tagName.ToLower()));
                }
            }
            return values;
        }

        ///<summary>
        ///</summary>
        ///<param name="id"></param>
        public IHTMLElement GetElementById(string id)
        {
            return _all
                .FirstOrDefault(e =>
                                string.Compare(e.id, id, StringComparison.InvariantCultureIgnoreCase) == 0);
        }

        ///<summary>
        ///</summary>
        ///<param name="name"></param>
        ///<returns></returns>
        public List<IHTMLElement> GetElementsByName(string name)
        {
            return _all
                .Where(e =>
                    {
                        string n = (string)e.getAttribute("name", 2);
                        if (!string.IsNullOrEmpty(n))
                        {
                            return string.Compare(n, name, StringComparison.InvariantCultureIgnoreCase) == 0;
                        }
                        return false;
                    }).ToList();
        }

        #endregion

        #region Private Methods

        private void ParseViewState()
        {
            HTMLInputElementClass viewStateNode = (HTMLInputElementClass)GetElementById("__VIEWSTATE");
            if (viewStateNode != null)
            {
                string viewStateFieldValue = viewStateNode.value;
                if (!string.IsNullOrEmpty(viewStateFieldValue))
                {
                    try
                    {
                        ViewStateParser parser = new ViewStateParser(viewStateFieldValue);
                        _viewState = parser.ViewState;
                        _controlState = parser.ControlState;
                    }
                    catch (Exception ex)
                    {
                        throw new Exception("Error parsing ViewState", ex);
                    }
                }
            }
        }

        #endregion

    }
}